--[[
	Scans the auction house with configurable requests.
	
	Copyright (C) Udorn (Blackhand)
	
	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.	
--]]
   
vendor.Scanner = vendor.Vendor:NewModule("Scanner", "AceEvent-3.0", "AceHook-3.0");
local L = vendor.Locale.GetInstance()
local self = vendor.Scanner;
local log = vendor.Debug:new("Scanner")

local SCANNER_VERSION = 7
local QUALITY_INDEX = {
	[0] = vendor.Format.ColorizeQuality(L["Poor"], 0), 
	[1] = vendor.Format.ColorizeQuality(L["Common"], 1),
	[2] = vendor.Format.ColorizeQuality(L["Uncommon"], 2),
	[3] = vendor.Format.ColorizeQuality(L["Rare"], 3),
	[4] = vendor.Format.ColorizeQuality(L["Epic"], 4),
	[5] = vendor.Format.ColorizeQuality(L["Legendary"], 5),
	[6] = vendor.Format.ColorizeQuality(L["Artifact"], 6),
} 
local MIN_QUALITIES = {
	[QUALITY_INDEX[0]] = 0,
	[QUALITY_INDEX[1]] = 1,
	[QUALITY_INDEX[2]] = 2,
	[QUALITY_INDEX[3]] = 3,
	[QUALITY_INDEX[4]] = 4,
	[QUALITY_INDEX[5]] = 5,
	[QUALITY_INDEX[6]] = 6,
}

--[[
	Initializes the snapshots for the faction and neutral auction houses.
--]]
local function _InitSnapshots()
	self.snapshots = {
		[true] = vendor.ScanSnapshot:new(self.db.realm.snapshot),
		[false] = vendor.ScanSnapshot:new(self.db.factionrealm.snapshot),
		["inverse"] = vendor.ScanSnapshot:new(self.db._factionrealm.snapshot)
	};
end

StaticPopupDialogs["SCANNER_DB_RESET"] = {
	text = L["Do you really want to reset the database?"];
	button1 = L["Yes"],
	button2 = L["No"],
	OnAccept = function()
		self.db.factionrealm.snapshot = {}
		self.db.realm.snapshot = {}		
		vendor.Vendor:Print(L["Database of scan snapshots for current realm and server where reset."]);
		_InitSnapshots();
	end,
	timeout = 0,
	whileDead = 1,
	hideOnEscape = 1
}	

--[[
	Migrates the database.
--]]
local function _MigrateDb(self)
	local oldDb = vendor.Vendor.oldDb:AcquireDBNamespace("Scanner")
	if (not self.db.factionrealm.version or self.db.factionrealm.version < 7) then
		-- migrate old "realm" to "factionrealm"
		self.db.factionrealm.snapshot = oldDb.realm.snapshot or {}
		oldDb.realm.snapshot = nil
	end	
	if (not self.db.realm.version or self.db.realm.version < 7) then
		-- migrate old "server" to "realm"
		self.db.realm.snapshot = oldDb.server.snapshot or {}
		oldDb.server.snapshot = nil
	end	
	if (not self.db.profile.version or self.db.profile.version < 7) then
		self.db.profile.minQuality = MIN_QUALITIES[self.db.profile.minQuality] or 1
	end
	self.db.factionrealm.version = SCANNER_VERSION
	self.db.realm.version = SCANNER_VERSION
	self.db.profile.version = SCANNER_VERSION
end

--[[
	Sets the given value in the profile.
--]]
local function _SetValue(info, value)
	if (info.arg == "minQuality") then
		self.db.profile[info.arg] = value - 1
	else
		self.db.profile[info.arg] = value
	end
end

--[[
	Returns the given value from the profile.
--]]
local function _GetValue(info)
	if (info.arg == "minQuality") then
		return self.db.profile[info.arg] + 1
	end
	return self.db.profile[info.arg]
end

--[[
	Sorts the scnasnaphot listeners according their order.
--]]
local function _SortScanSnapshotListeners(a, b)
	return a.order < b.order;
end

--[[
	Options for this module.
--]]
local function _CreateOptions()
	vendor.Vendor.options2.args.scanner = {
		type = "group",
		name = L["Scanner"],
		desc = L["Scanner"],
		args = {
			minQuality = {
				type = "select",
				name = L["Minimum quality"],
				desc = L["Selects the minimum quality for items to be scanned in the auction house."],
				get = _GetValue,
				set = _SetValue,
				values = {QUALITY_INDEX[0], QUALITY_INDEX[1], QUALITY_INDEX[2], QUALITY_INDEX[3], QUALITY_INDEX[4], QUALITY_INDEX[5], QUALITY_INDEX[6]},
				arg = "minQuality",
				order = 1,
			},
			reset = {
				type = "execute",
				name = L["Reset database"],
				desc = L["Resets the database of scan snapshots for the current realm and server."],
				func = function() StaticPopup_Show("SCANNER_DB_RESET") end,
				order = 2,
			},			
		}
	}
	vendor.Vendor.AceConfigRegistry:RegisterOptionsTable("AuctionMaster Scanner", vendor.Vendor.options2.args.scanner)
	vendor.Vendor.AceConfigDialog:AddToBlizOptions("AuctionMaster Scanner", L["Scanner"], "AuctionMaster")
end

--[[
	Informs any listener about the update of the given snapshot.
--]]
local function _NotifySnapshotUpdate(self, snapshot, isNeutral)
	if (self.scanResultListeners) then
		for _, listener in ipairs(self.scanResultListeners) do
			assert(listener.listener.ScanSnapshotUpdated);
			listener.listener:ScanSnapshotUpdated(snapshot, isNeutral);
		end
	end
	vendor.Vendor:Debug("leave");
end

--[[
	Informs any listener about the abortion of a scan.
--]]
local function _NotifySnapshotAborted(self)
	if (self.scanResultListeners) then
		for _, listener in ipairs(self.scanResultListeners) do
			if (listener.listener.ScanAborted) then
				vendor.Vendor:Debug("inform listener about abortion");
				listener.listener:ScanAborted();
			end
		end
	end
end

--[[
	Cleanup code at the end of a scan.
--]]
local function _StopScan(self, complete)
	self.scanTask = nil;
	_NotifySnapshotAborted(self);
	if (self.scanDialog:IsVisible()) then
		self.scanDialog:Hide();
		PlaySound("AuctionWindowClose")
	end
	self:SendMessage("AUCTION_MASTER_STOP_SCAN", complete)
end

--[[
	Initializes the module.
--]]
function vendor.Scanner:OnInitialize()
	log:Debug("OnInitialize")
	self.db = vendor.Vendor.db:RegisterNamespace("Scanner", {
		factionrealm = {
			snapshot = {},
		},
		_factionrealm = {
			snapshot = {},
		},
		realm = {
			snapshot = {}		
		},
		profile = {
			minQuality = 1,
		}
	});
	_MigrateDb(self)
	_CreateOptions();	
	self.lru = vendor.LruCache:new(25);
end

--[[
	Initializes the scanner at startup and registers for the needed events.
--]]
function vendor.Scanner:OnEnable()
	self.scanDialog = vendor.ScanDialog:new();
	_InitSnapshots();
	self:RegisterEvent("AUCTION_ITEM_LIST_UPDATE");
	self:RegisterEvent("AUCTION_HOUSE_CLOSED");
	--self:Hook("AuctionFrameBrowse_OnEvent", true);
	self:RawHook("AuctionFrameBrowse_OnEvent", true)
	self:Hook("CanSendAuctionQuery", true);
end

--[[
	Performs a scan for the given item. nil will trigger a complete scan.
	@param name the name pattern or "" for a complete scan.
--]]
function vendor.Scanner:Scan(itemLink, isFastScan, func, arg1)
	if (self.scanTask) then
		vendor.Vendor:Error(L["There is already a running scan."]);
	else
		self.scanTask = vendor.ScanTask:new(itemLink, self.db.profile.minQuality, isFastScan);
		vendor.TaskQueue:AddTask(self.scanTask, func, arg1)
	end	
end

--[[
	Hides the auction house browse buttons, used during scans.
--]]
function vendor.Scanner:HideAuctionHouseBrowseButtons()
   	for i=1, NUM_BROWSE_TO_DISPLAY do
      	local button = getglobal("BrowseButton"..i);
      	button:Hide();
   	end
   	BrowsePrevPageButton:Disable();
   	BrowseNextPageButton:Disable();
   	BrowseSearchCountText:Hide();
end

--[[
	Shows some status information for the scan on the list tab of the auction house.
--]]
function vendor.Scanner:ShowScanStatus(msg)
	self.scanDialog:ShowProgress(msg);
end

--[[
	Events triggered if the auction item list was updated. Will
	inform any current ScanTask about it.
--]]
function vendor.Scanner:AUCTION_ITEM_LIST_UPDATE()
	if (self.scanTask) then
		self.scanTask:AuctionListUpdate()
		self:HideAuctionHouseBrowseButtons()
	end
end

--[[
	Events for auction house closes. Will cancel any running scan task.
--]]
function vendor.Scanner:AUCTION_HOUSE_CLOSED()
	if (self.scanTask) then
		self.scanTask:Cancel();
	end
	self.scanDialog:Hide();
end

--[[
	Hook to prevent blizzard seeing the auctions while we are scanning.
--]]
function vendor.Scanner:AuctionFrameBrowse_OnEvent(...)
	self.hooks["AuctionFrameBrowse_OnEvent"](...)
	if (self.scanTask) then
		self:HideAuctionHouseBrowseButtons()
	end
end

--[[
	Hook to prevent blizzard to flicker the "Search" button.
--]]
function vendor.Scanner:CanSendAuctionQuery()
	if (not self.scanTask) then
		return self.hooks.CanSendAuctionQuery();
   	end
	return nil;
end

--[[
	Returns the result of "CandSendQuery" bypassing the own hook.
--]]
function vendor.Scanner:MaySendAuctionQuery()
	return self.hooks.CanSendAuctionQuery();
end

--[[
	Returns the most current ScanSet for the specified item.
	@param isNeutral has to be set to true, for neutral auction houses.
	@param itemLink the item to be selected.
--]]
function vendor.Scanner:GetScanSet(isNeutral, itemLink, bInverse)
	local key = vendor.Items:GetItemLinkKey(itemLink).."-"..vendor.Format.BoolToStr(isNeutral);
	if bInverse then
		key = key .. "-" .."inverse"
	end
	local snap = self.lru:Get(key);
	if (not snap) then
		local snapshot = self.snapshots[isNeutral]; 
		if bInverse then
			snapshot=self.snapshots["inverse"];
		end
		snap = snapshot:GetScanSet(isNeutral, itemLink); 
		self.lru:Put(key, snap);
	end
	return snap;
end

--[[
	Registers a listener for updates for the scan snapshots. The order sorts the listeners.
	Small values come first. nil is interpreted as 1000.
	
	The listener has to implement the method "ScanSnapshotUpdated", with the
	following synposis:
	@param scanSnapshot the updated ScanSnapshot.
	@param isNeutral determines whether the snapshot was for a neutral auction house.
--]] 
function vendor.Scanner:AddScanSnapshotListener(listener, order)
	if (not self.scanResultListeners) then
		self.scanResultListeners = {};
	end
	table.insert(self.scanResultListeners, {listener = listener, order = order or 1000});
	table.sort(self.scanResultListeners, _SortScanSnapshotListeners);
end

--[[
	Adds a listener for items found during auction house scans.
	The listener has to implement the method "ItemScanned" with the following synopsis:
	@param index the current auction house index
	@param itemLink the link of the scanned item.
	@param itemName the name of the item.
--]]
function vendor.Scanner:AddScanItemListener(listener)
	if (not self.scanItemListeners) then
		self.scanItemListeners = {};
	end
	table.insert(self.scanItemListeners, listener);
end

--[[
	Informs the listeners about the given scanned item.
--]]
function vendor.Scanner:NotifyItemScanned(index, itemLink, itemName, count, bid, buyout, isFastScan, highBidder)
	if (self.scanItemListeners) then
		for _, listener in ipairs(self.scanItemListeners) do
			local askForBuy, reason, doBid, doBuyout = listener:ItemScanned(index, itemLink, itemName, isFastScan, highBidder);
			if (askForBuy) then
				vendor.Sniper:StartPendingBuy(itemName, count, bid, buyout);
				vendor.Vendor:Debug("ask to buy count: "..count.." bid: "..bid)
				self.scanDialog:AskToBuy(itemLink, count, bid, buyout, reason, index, doBid, doBuyout, highBidder);
				vendor.Sniper:WaitForPendingBuy();
			end
		end		
	end
end

--[[
	Stops the current scan, if any.
--]]
function vendor.Scanner:StopScan()
	if (self.scanTask) then
		self.scanTask:Cancel();
	end	
end

--[[
	Returns true, if the scanner is currently performing a scan.
--]]
function vendor.Scanner:IsScanning()
	return self.scanTask ~= nil;
end

--[[
	Sets the given complete snapshot produced by a scan task.
	Will assume that the current scan task is finished now.
--]]
function vendor.Scanner:SetSnapshot(snapshot, isNeutral)
	self.lru:Clear();
	self.snapshots[isNeutral] = snapshot;
	if (isNeutral) then
		self.db.realm.snapshot = snapshot:GetData();
	else
		self.db.factionrealm.snapshot = snapshot:GetData();
	end
	_NotifySnapshotUpdate(self, snapshot, isNeutral);
	_StopScan(self, true);
end

--[[
	Sets the given complete item snapshot produced by a scan task.
	Will assume that the current scan task is finished now.
--]]
function vendor.Scanner:SetItemSnapshot(itemLink, snapshot, isNeutral)
	local key = vendor.Items:GetItemLinkKey(itemLink).."-"..vendor.Format.BoolToStr(isNeutral);
	self.lru:Remove(key);
	local snap = self.snapshots[isNeutral];
	snap:DeleteResults(itemLink);
	snap:InsertResults(snapshot);
	_NotifySnapshotUpdate(self, snapshot, isNeutral);
	_StopScan(self, true);
end

--[[
	Lets the scan task inform the scanner, that it has abandoned the scan.
--]]
function vendor.Scanner:AbandonScan()
	_StopScan(self, false);
end

--[[
	Signal from the scan task, that the scan is about to be started.
--]]
function vendor.Scanner:NotifyStartScan()
	self:SendMessage("AUCTION_MASTER_START_SCAN", "start scan")
end
